package grex.genes;
import grex.Neighbour;
import grex.Options;
import grex.Prediction;
import grex.PredictionContainer;

import java.util.ArrayList;
import java.util.Collections;
import java.util.Hashtable;
import java.util.Map.Entry;

public class KnnAtt extends Gene implements ITerminal{

   private static final int NROFCILDREN = 1;
   private int k = -1,  classes;
   private double kPercent;
   private PredictionContainer neigbours;
   
 //  private double prob = 0;
   private double lowerProb = -1;
   private double instProb=0;
   private double[] attributeWeights;
   int count =0;
   private boolean initialized=false;
   
   public KnnAtt() {
       super(NROFCILDREN);
       kPercent = Options.rnd.nextDouble();       
   }   

    public synchronized void execute(Gene parent, PredictionContainer data, int mode) throws GeneException {
        
        this.parent = parent;
       if (mode==Gene.TRAIN_MODE) {
           neigbours=data;
           count=data.values().size();           
           if (k == -1) {
               k = (int) (kPercent*(ops.getKNN_MAX()-ops.getKNN_MIN()))+ops.getKNN_MIN();
               if (k % 2 == 0) {
                   k++;
               }
           }
       }
       for (Prediction p : data.values()) {
           p.setPrediction(calcValue(p.getInstance()));
           p.setProb(instProb);
           p.setLowerProb(lowerProb);
           p.setLeaf(this);
       }

   }

   private double calcValue(double[] instance) throws GeneException {
      ArrayList<Neighbour> knns;
       if(ops.isKNN_WEIGHTED_ATTRIBUTES()){
           attributeWeights =((KnnAttWeights) this.getChildren()[0]).getAttributeWeights();
           knns = getNearestAttrKnns(instance, ops.getNR_OF_CONSIDERED_NEIGBHOURS());           
       }else
             knns = environment.getKNNs(instance, k);
       double classValue = -666;
       if (knns.size() != 0) {
           if(ops.getPROBLEM_TYPE()==ops.CLASSIFICATION){
               Hashtable<Double, Integer> counts = getClassCount(knns);
               if( ops.isKNN_WEIGHTED_VOTING())
                 classValue = calcWeightedDistMajorityClass(counts, knns);
               else //normal knn
                 classValue = calcMajorityClass(counts);
               classes = environment.getNrOfTargetCategories();
               instProb = (double) counts.get(classValue);
               instProb = (instProb+1) / (k+classes);
               return classValue;
           }else{
               double avgPred=0;
               for(int i = 0; i < k;i++){
                   double[] current = knns.get(i).getInstance();
                   avgPred+=current[current.length-1]/k;
               }
               instProb=0;
               lowerProb=0;
               int lowerCount=0;
               int probCount=0;
               for(int i = 0; i < k;i++){
                   double[] current = knns.get(i).getInstance();               
                   double target = current[current.length-1];
                   if(avgPred>target){
                        instProb+=(Math.abs(target-avgPred)/target);                   
                        probCount++;
                   }else{
                        lowerProb+=(Math.abs(target-avgPred)/target);                   
                        lowerCount++;
                   }
               }
               if(probCount>0)
               instProb=instProb/probCount;
               if(lowerCount>0)
               lowerProb=lowerProb/lowerCount;
               
               return avgPred;
           }
       }
       return -666;
   }

   private Hashtable<Double, Integer> getClassCount(ArrayList<Neighbour> knns) {
       //calc class Distribution
       Hashtable<Double, Integer> counts = new Hashtable<Double, Integer>();
       double targetValue;
       double realNeighbours = 0;
       for (int i = 0; realNeighbours < k && i < knns.size(); i++) {
           if (!knns.get(i).getFold().equalsIgnoreCase(ops.getTestFOLD())) {
               double[] n = knns.get(i).getInstance();
               realNeighbours++;
               targetValue = n[n.length - 1];
               Integer count = counts.get(targetValue);
               if (count == null) {
                   counts.put(targetValue, 1);
               } else {
                   counts.put(targetValue, count + 1);
               }
          }
       }
       return counts;
   }

   private double calcWeightedDistMajorityClass(Hashtable<Double, Integer> counts,ArrayList<Neighbour> knns) {
       double key=0,max=0;
       
       Double[] classes = counts.keySet().toArray(new Double[0]);
       double[] weight = new double[counts.keySet().size()];
       double totDistance=0;
       int j = knns.size();
       for(Neighbour k: knns){
           double[] n = k.getInstance();
           for(int i = 0 ; i < classes.length;i++){
               if(n[n.length-1]==classes[i]){
                       weight[i]+=k.getDistance();
                       totDistance+=k.getDistance();
                       break;
               }
           }           
       }
       
       for ( int i = 0; i<classes.length; i++) {
           double count = counts.get(classes[i]);
           if (max < count*weight[i]/totDistance) {
               max = count*weight[i]/totDistance;
               key = classes[i];
           }
       }
       return key;
   }
      private ArrayList<Neighbour> getNearestAttrKnns(double[] instance,int nrOfConsideredNeigbours) {
       double key=0,max=0;
       ArrayList<Neighbour> knns = environment.getKNNs(instance, nrOfConsideredNeigbours);

       double totDistance=0;
       int j = knns.size();
       ArrayList<Neighbour> consideredKnns = new ArrayList<Neighbour>();
       for(int z = 0; z < nrOfConsideredNeigbours;z++){
           Neighbour k =  knns.get(z).clone();
           consideredKnns.add(k);
           double[] n = k.getInstance();
           //Calc weighted distance
           double attrDist = 0;
           for(int i = 0;i < attributeWeights.length; i++){
               if(environment.isNumericColumn(i))
                    attrDist += Math.pow(instance[i]-n[i],2)*attributeWeights[i]; 
               else{//Categorical attribute
                   if (instance[i] != n[i]) {
                        attrDist += 1 * attributeWeights[i];
                    }
               }
           }
           k.setDistance(Math.sqrt(attrDist));
       }
       Collections.sort(consideredKnns);
       return consideredKnns;
 
   }
   

   private double calcMajorityClass(Hashtable<Double, Integer> counts) {
       Entry<Double, Integer> max = null;
       for (Entry<Double, Integer> e : counts.entrySet()) {
           if (max == null || max.getValue() < e.getValue()) {
               max = e;
           }
       }
       return max.getKey();
   }

   public double getKPercent() {
       return kPercent;
   }

   public void setKPercent(double kPercent) {
       this.kPercent = kPercent;
   }

    public double getLowerProb() {
        return lowerProb;
    }

   public void setNeigbours(PredictionContainer neigbours) {
       this.neigbours = neigbours;
   }

   public PredictionContainer getNeigbours() {
       return neigbours;
   }

   public void setProb(double prob){
       this.instProb=instProb;
   }

   public void setK(int k){
       this.k=k;
   }
public void setCount(int count){
           this.count=count;
       }

   public Object clone() {
       KnnAtt newKnnAtt;
       newKnnAtt = new KnnAtt();
       newKnnAtt.neigbours=neigbours.shallowClone();
       newKnnAtt.ops = ops;  
       newKnnAtt.environment = environment;
       newKnnAtt.count = count;
       newKnnAtt.instProb=instProb;
       newKnnAtt.lowerProb=lowerProb;
       newKnnAtt.k=k;
       newKnnAtt.parent = parent;
       newKnnAtt.attributeWeights = attributeWeights;
       cloneChildren(newKnnAtt);       
       return newKnnAtt;
   }

   public String toString() {
       String s =  "KNN" + k + "(" + neigbours.size() + ")";
       return s;
   }

   public double getProb() {
      return instProb;
   }

   public int getCount() {
       return count;
   }

    public double getLowProb() {
        return lowerProb;
    }

    public boolean isInitialized() {
        return initialized;
    }

    public void setInitialized(boolean initialized) {
        this.initialized = initialized;
    }
    public void initAttributesWeights(){
        attributeWeights = new double[ops.getNR_OF_INPUT_COLUMNS()];
        for(int i=0;i<attributeWeights.length;i++)
            attributeWeights[i]= ops.rnd.nextDouble();
    }

    public double[] getAttributeWeights() {
        return attributeWeights;
    }

    public void setAttributeWeights(double[] attributeWeights) {
        this.attributeWeights = attributeWeights;
    }
}
